﻿/**
 * Peter
 * Created by: Peter Development Team
 *    http://peter.codeplex.com/
 * 
 * GNU General Public License version 2 (GPLv2)
 *    http://peter.codeplex.com/license
 *
 *  This code is provided on an AS IS basis, with no WARRANTIES,
 *  CONDITIONS or GUARANTEES of any kind.
 *
 **/

using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Collections.Specialized;
using System.ComponentModel.Composition;
using System.IO;
using System.Linq;
using System.Text.RegularExpressions;
using System.Windows.Input;
using ICSharpCode.AvalonEdit;
using Peter.Common;
using Peter.Common.AutoComplete;
using Peter.Common.Interfaces;
using Peter.Common.MainMenu;
using Peter.Common.Services;
using Peter.Common.Utilities;
using Peter.Data;
using Peter.Logic.AvalonEdit;
using Peter.Logic.Interfaces;
using Peter.Properties;

namespace Peter.Logic
{
   /// <summary>
   /// Model for a terminal
   /// </summary>
   public sealed class ModelTerminal : EditablePeterToolWindow, IEditorNeeded, IAutoCompleteDataProvider
   {
      private int m_CaretPosition;
      private string m_CommandLine;
      private OptionsGeneral m_General;
      private TextEditor m_OutputEditor;
      private int m_CurrentCompleteIndex;
      private RelayCommand m_ClearCommand;
      private IPeterOptions m_PeterOptions;
      private CommandPrompt m_CommandPrompt;
      private readonly ErrorLineHighlighter m_ErrorHighlighter;
      private readonly StringCollection m_CurrentCompleteItems;

      /// <summary>
      /// Initializes a new terminal
      /// </summary>
      public ModelTerminal ()
      {
         this.Title = "Terminal";
         this.ContentId = Guids.TERMINAL;
         this.Icon = MainMenuIcon.Terminal;
         this.ToolTip = "Execute terminal/console commands";
         this.m_CurrentCompleteItems = new StringCollection ();
         this.m_ErrorHighlighter = new ErrorLineHighlighter ();
      }

      /// <summary>
      /// Gets the command prompt.
      /// </summary>
      public CommandPrompt CommandPrompt
      {
         get { return this.m_CommandPrompt; }
         private set
         {
            this.m_CommandPrompt = value;
            this.RaisePropertyChanged ("CommandPrompt");
         }
      }

      /// <summary>
      /// Gets the options.
      /// </summary>
      [Import (typeof (IPeterOptions))]
      public IPeterOptions Options
      {
         get { return this.m_PeterOptions; }
         set
         {
            this.m_PeterOptions = value;
            if (!this.m_PeterOptions.IsLoaded)
               this.m_PeterOptions.Loaded += this.OnOptionsLoaded;
         }
      }

      /// <summary>
      /// Gets the general options.
      /// </summary>
      private OptionsGeneral GeneralOptions
      {
         get
         {
            if (this.m_General == null)
            {
               this.m_General = (OptionsGeneral)this.Options.GetOptionPage ("General");
               if (this.m_General == null)
                  this.Options.Loaded += this.OnOptionsLoaded;
            }
            return this.m_General;
         }
      }

      /// <summary>
      /// Gets or Sets the current command line text.
      /// </summary>
      public string CommandLine
      {
         get { return m_CommandLine; }
         set
         {
            this.m_CommandLine = value;
            this.RaisePropertyChanged ("CommandLine");
         }
      }

      /// <summary>
      /// Gets the command history.
      /// </summary>
      public Collection <string> CommandHistory
      {
         get { return this.GeneralOptions == null ? new Collection <string> () : this.GeneralOptions.CommandHistory; }
      }

      /// <summary>
      /// Gets the command to clear the output.
      /// </summary>
      public ICommand ClearCommand
      {
         get { return m_ClearCommand ?? (this.m_ClearCommand = new RelayCommand (this.OnClear)); }
      }

      /// <summary>
      /// Gets or Sets the caret position.
      /// </summary>
      public int CaretPosition
      {
         get { return m_CaretPosition; }
         set
         {
            this.m_CaretPosition = value;
            this.RaisePropertyChanged ("");
         }
      }

      /// <summary>
      /// Occurs when the options are loaded.
      /// </summary>
      /// <param name="sender">Options object.</param>
      /// <param name="e">EventArgs</param>
      private void OnOptionsLoaded (object sender, EventArgs e)
      {
         var options = ServiceManager.Get<IPeterOptions> ();
         options.Loaded -= this.OnOptionsLoaded;
         this.m_General = (OptionsGeneral)options.GetOptionPage ("General");
         this.RaisePropertyChanged ("CommandHistory");
      }

      /// <summary>
      /// Clears the editor.
      /// </summary>
      /// <param name="obj">Command parameter.</param>
      private void OnClear (object obj)
      {
         this.m_ErrorHighlighter.LineNumbers.Clear ();
         this.m_OutputEditor.Clear ();
      }

      /// <summary>
      /// Updates the displayed output.
      /// </summary>
      /// <param name="text">Text to add to output.</param>
      /// <param name="isError">True if text is an error, otherwise false.</param>
      private void UpdateOutput (string text, bool isError = false)
      {
         this.m_OutputEditor.AppendText (text);
         if (isError)
         {
            this.m_ErrorHighlighter.LineNumbers.Add (this.m_OutputEditor.LineCount);
            this.m_OutputEditor.TextArea.TextView.Redraw ();
         }
         this.m_OutputEditor.AppendText (Environment.NewLine);
         this.m_OutputEditor.ScrollToEnd ();
      }

      /// <summary>
      /// Runs the given Script...
      /// </summary>
      /// <param name="script">Script to run (Commands should be separated by a new line '\n').</param>
      /// <param name="workingDirectory">Directory to run script.</param>
      public void RunScript (string script, string workingDirectory)
      {
         var commands = Regex.Split (script, Environment.NewLine);
         foreach (var command in commands)
         {
            if (!string.IsNullOrEmpty (command))
            {
               this.CommandPrompt.RunCommand (command);
            }
         }
      }

      /// <summary>
      /// Runs the command line.
      /// </summary>
      public void RunCommandLine ()
      {
         var command = this.CommandLine;
         this.CommandPrompt.RunCommand (command);

         // Remove if alread added...
         if (this.GeneralOptions.CommandHistory.Contains (command))
            this.GeneralOptions.CommandHistory.Remove (command);
         // Add...
         this.GeneralOptions.CommandHistory.Add (command);
         // Trim...
         while (this.GeneralOptions.CommandHistory.Count > Settings.Default.MaxCommandHistory)
            this.GeneralOptions.CommandHistory.RemoveAt (this.GeneralOptions.CommandHistory.Count - 1);
         this.RaisePropertyChanged ("CommandHistory");

         this.CommandLine = string.Empty;
      }

      /// <summary>
      /// Closes the dock item.
      /// </summary>
      /// <param name="obj">Command parameter.</param>
      protected override void OnClose (object obj)
      {
         this.CommandPrompt.Close ();
         base.OnClose (obj);
      }

      /// <summary>
      /// Hides the dock item.
      /// </summary>
      /// <param name="obj">Command parameter.</param>
      protected override void OnHide (object obj)
      {
         this.OnClose (obj);
      }

      /// <summary>
      /// Sets the editor.
      /// </summary>
      /// <param name="editor">The editor control.</param>
      public void SetEditor (TextEditor editor)
      {
         this.m_OutputEditor = editor;
         this.m_OutputEditor.TextArea.TextView.LineTransformers.Add (this.m_ErrorHighlighter);

         var commandPrompt = new CommandPrompt ();
         commandPrompt.OutputRecieved += this.UpdateOutput;
         commandPrompt.Initialize ();
         this.CommandPrompt = commandPrompt;
      }

      /// <summary>
      /// Gets the list of auto complete items.
      /// </summary>
      /// <param name="filter">Text filter to apply.</param>
      /// <returns>Collection of auto complete items.</returns>
      public IEnumerable <string> GetAutoCompleteItems (string filter)
      {
         if (this.GeneralOptions != null)
         {
            var commandHistory = this.GeneralOptions.CommandHistory;
            foreach (var completeItems in commandHistory.Where (si => si.ContainsIgnoreCase (filter)))
            {
               yield return completeItems;
            }
         }
      }

      /// <summary>
      /// Performs the edit action.
      /// </summary>
      /// <param name="obj">Edit action.</param>
      protected override void OnEdit (object obj)
      {
         var editAction = (EditAction)obj;
         switch (editAction)
         {
            case EditAction.Copy:
               this.m_OutputEditor.Copy ();
               break;
            case EditAction.SelectAll:
               this.m_OutputEditor.SelectAll ();
               break;
         }
         base.OnEdit (obj);
      }

      /// <summary>
      /// Attempts to complete the command line.
      /// </summary>
      public void CompleteCommandLine ()
      {
         var cmdLine = this.CommandLine;
         var lastSpace = GetLastSpace (cmdLine);

         if (this.m_CurrentCompleteItems.Count == 0)
         {
            var currentPath = cmdLine.Substring (lastSpace);
            currentPath = currentPath.Replace ('/', '\\').Replace ("\"", "");
            var currentDir = currentPath.Substring (0, currentPath.LastIndexOf ('\\') + 1);
            if (!Directory.Exists (currentDir)) currentDir = this.CommandPrompt.WorkingDirectory;
            var filter = currentPath.Substring (currentPath.LastIndexOf ('\\') + 1);
            var dirs = Directory.GetDirectories (currentDir, filter + "*", SearchOption.TopDirectoryOnly);
            this.m_CurrentCompleteItems.AddRange (dirs.Select (d => (d.Contains (" ") ? "\"" + d + "\"": d)).ToArray ());
            var files = Directory.GetFiles (currentDir, filter + "*", SearchOption.TopDirectoryOnly);
            this.m_CurrentCompleteItems.AddRange (files);
            this.m_CurrentCompleteIndex = 0;
         }

         if (this.m_CurrentCompleteItems.Count > 0)
         {
            cmdLine = cmdLine.Remove (lastSpace);
            cmdLine += this.m_CurrentCompleteItems[this.m_CurrentCompleteIndex];

            this.CommandLine = cmdLine;
            this.m_CurrentCompleteIndex++;
            this.CaretPosition = cmdLine.Length;
            if (this.m_CurrentCompleteIndex >= this.m_CurrentCompleteItems.Count)
               this.m_CurrentCompleteIndex = 0;
         }
      }

      /// <summary>
      /// Gets the index of the last space in the command line.
      /// </summary>
      /// <param name="cmdLine">The command line to get last space for.</param>
      /// <returns>The offset of the last space in the command line.</returns>
      private static int GetLastSpace (string cmdLine)
      {
         int lastSpace = 0;
         bool inQuotes = false;
         for (int i = 0; i < cmdLine.Length; i++)
         {
            if (!inQuotes && cmdLine [i] == '"')
               inQuotes = true;
            else if (inQuotes && cmdLine [i] == '"')
               inQuotes = false;
            else if (!inQuotes && cmdLine [i] == ' ')
               lastSpace = i + 1;
         }
         return lastSpace;
      }

      /// <summary>
      /// Clears out the current complete data.
      /// </summary>
      public void ClearCommandLineComplete ()
      {
         this.m_CurrentCompleteItems.Clear ();
         this.m_CurrentCompleteIndex = 0;
      }
   }
}
